Math Sci Life Code Log in

Codes

Code your ideas for understanding of natural systems

Updated at 2021.1.3 Updated at 2020.11.01 Updated at 2020.09.28

Intro

Markdown 편집기를 Blazor 프레임워크를 활용하여 웹페이지로 만들어 보자. VSCode만한 편집기가 없지만, 웹에서 직접 간단히 편집을 할 수 있고 나의 용도에 맞게 커스터마이즈할 수 있기 때문에 시도해 볼 만하다. 구글링을 통해 여러 가지 정보를 참고했지만, 다음 사이트를 가장 많이 참조하였다.

Creating an ASP.NET Core Markdown TagHelper and Parser

Install Nuget Package

  • Markdig 0.2.1: 기본 기능만 사용할 경우
  • Westwind.AspNetCore.Markdown 3.4.0: Pipeline 등 고급 기능을 사용할 경우

Making Simple Editor at Blazor WebAssembly

만들고자 하는 것은 3개의 칼럼으로 나누어져 있다.

  • 첫번째는 마크다운 텍스트 입력,
  • 두번째는 마크업(HTML)으로 변환된 문서를,
  • 세번째는 그 HTML 문서를 Viewer로 보여준다.

Markdown Editor in Blazor

Start new Blazor WebAssembly project

문법에 대한 상세한 설명을 제외하고 간단하게 핵심 스토리만 이야기하면,

  • textarea에 텍스트가 변했을 때 OnTextChanged 함수를 호출하고,
  • 입력된 문자열을 MarkdownPipelineBuilder를 이용하여 HTML로 변환한다.
  • 변환된 문자열을 MarkupString으로 타입을 변환해 HTML Viewer로 보여준다.

Making MDEditor razor file

MDEditor.razor: 세 개의 칼럼으로 나누고 textarea 입력 발생 시 OnTextChanged 이벤트 처리 함수를 연결 시키고, 변환된 문자열과 그것을 MarkupString 타입 변환 것을 보여주게 한다.

@page "/MDEditor"

<div class="row">
    <div class="col-4">
        <textarea rows="20" class="form-control" @oninput="OnTextChanged"></textarea>
    </div>
    <div class="col-4">
        @Preview
    </div>
    <div class="col-4">
        @((MarkupString)Preview)
    </div>
</div>

Making MDEditor razor cs file

MDEditor.razor.cs: Razor파일 내에 cs 코드를 넣을 수도 있지만, 여기서는 개별 파일을 만들어서 OnTextChanged 이벤트 처리 함수를 구현한다. ChangedEventArgs를 파라미터로 받아들여 파싱을 하고, 자동으로 화면을 업데이트하기 위해 StateHasChanged 함수를 비동기로 호출한다.

using Microsoft.AspNetCore.Components;
using Markdig;

namespace ArchivesdjWork1.Pages
{
    public partial class MDEditor : ComponentBase
    {
        public string Preview { get; set; }

        protected void OnTextChanged(ChangeEventArgs args)
        {
            Preview = Parse(args.Value.ToString());
            InvokeAsync(() => StateHasChanged());
        }

        public static string Parse(string markdown)
        {
            var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
            return Markdown.ToHtml(markdown, pipeline);
        }
    }
}

Making Simple Editor at Blazor Server

Blazor Server 프레임워크에 동일한 기능을 구현해 보자. 텍스트가 변경될 때마다 변환 함수를 호출하는 것은 속도도 늦고 비효율적이기 때문에 Westwind.AspNetCore.Markdown Nuget Package를 설치하여 업그레이드 해보자.

Making MarkdownParser Interface

IMarkdownParser: Parse 메서드를 멤버로 가지는 인터페이스를 만든다.

namespace ArchivesdjWork2.Pages.Markdown
{
    public interface IMarkdownParser
    {
        public string Parse(string markdown, bool sanitizeHtml = true);
    }
}

Making Wrapper around Markdig with Cached Instance

MarkdownParserMarkdig.cs: 상세한 함수 및 활용법은 Westwind.AspNetCore.Markdown를 찾아 봐야한다. 아직 다 파악하지 못했다. 기본 뼈대만 정리하면,

  • Constructor를 1개 만들어서 MarkdownPipeline을 생성한다.
  • IMarkdownParser 인터페이스를 상속 받았으므로, Parse함수를 구현한다. 상세 내용은 Markdig 라이브러리를 찾아 볼 필요가 있다.
  • MardownParserBase에서 상속 받은 2개의 함수를 구현한다.
    • Constructor에서 호출한 CreatePipelineBuilder를 구현한다. 생성시 다양한 옵션 처리를 하느라 복잡하게 긴데 내용은 심플하다.
    • CreateRenderer은 단순히 HTMLRenderer를 생성하여 호출한다.
using Markdig;
using Markdig.Extensions.AutoIdentifiers;
using Markdig.Renderers;
using System;
using System.IO;
using Westwind.AspNetCore.Markdown;

namespace ArchivesdjWork2.Pages.Markdown
{
    /// <summary>
    /// Wrapper around the MarkDig parser that provides a cached
    /// instance of the Markdown parser. Hooks up custom processing.
    /// </summary>
    public class MarkdownParserMarkdig : MarkdownParserBase, IMarkdownParser
    {
        public static MarkdownPipeline Pipeline;

        private readonly bool _usePragmaLines;

        public MarkdownParserMarkdig(bool usePragmaLines = false, bool force = false, Action<MarkdownPipelineBuilder> markdigConfiguration = null)
        {
            _usePragmaLines = usePragmaLines;
            if (force || Pipeline == null)
            {
                var builder = CreatePipelineBuilder(markdigConfiguration);
                Pipeline = builder.Build();
            }
        }

        /// <summary>
        /// Parses the actual markdown down to html
        /// </summary>
        /// <param name="markdown"></param>
        /// <returns></returns>        
        public override string Parse(string markdown, bool sanitizeHtml = true)
        {
            if (string.IsNullOrEmpty(markdown))
                return string.Empty;

            var htmlWriter = new StringWriter();
            var renderer = CreateRenderer(htmlWriter);

            Markdig.Markdown.Convert(markdown, renderer, Pipeline);

            var html = htmlWriter.ToString();

            html = ParseFontAwesomeIcons(html);

            return html;
        }

        public virtual MarkdownPipelineBuilder CreatePipelineBuilder(Action<MarkdownPipelineBuilder> markdigConfiguration)
        {
            MarkdownPipelineBuilder builder = null;

            // build it explicitly
            if (markdigConfiguration == null)
            {
                builder = new MarkdownPipelineBuilder()
                    .UseEmphasisExtras()
                    .UsePipeTables()
                    .UseGridTables()
                    .UseFooters()
                    .UseFootnotes()
                    .UseCitations()
                    .UseAutoLinks() // URLs are parsed into anchors
                    .UseAutoIdentifiers(AutoIdentifierOptions.GitHub) // Headers get id="name" 
                    .UseAbbreviations()
                    .UseYamlFrontMatter()
                    .UseEmojiAndSmiley(true)
                    .UseMediaLinks()
                    .UseListExtras()
                    .UseFigures()
                    .UseTaskLists()
                    .UseCustomContainers()
                    .UseMathematics()
                    .UseGenericAttributes();

                if (_usePragmaLines)
                    builder = builder.UsePragmaLines();

                return builder;
            }

            // let the passed in action configure the builder
            builder = new MarkdownPipelineBuilder();
            markdigConfiguration.Invoke(builder);

            if (_usePragmaLines)
                builder = builder.UsePragmaLines();

            return builder;
        }

        protected virtual IMarkdownRenderer CreateRenderer(TextWriter writer)
        {
            return new HtmlRenderer(writer);
        }
    }
}

Retrieving Instance of a Markdown Parser

MarkdownParserFactory: IMarkdownParser를 정적으로 1개 생성하여 GetParser 호출 시 반환하는 정적 클래스이다.

namespace ArchivesdjWork2.Pages.Markdown
{
    /// <summary>
    /// Retrieves an instance of a markdown parser
    /// </summary>
    public static class MarkdownParserFactory
    {
        /// <summary>
        /// Use a cached instance of the Markdown Parser to keep alive
        /// </summary>
        static IMarkdownParser CurrentParser;

        /// <summary>
        /// Retrieves a cached instance of the markdown parser
        /// </summary>                
        /// <param name="forceLoad">Forces the parser to be reloaded - otherwise previously loaded instance is used</param>
        /// <param name="usePragmaLines">If true adds pragma line ids into the document that the editor can sync to</param>
        /// <returns>Mardown Parser Interface</returns>
        public static IMarkdownParser GetParser(bool usePragmaLines = false,
                                                bool forceLoad = false)
        {
            if (!forceLoad && CurrentParser != null)
                return CurrentParser;

            CurrentParser = new MarkdownParserMarkdig(usePragmaLines, forceLoad);

            return CurrentParser;
        }
    }
}

Adding Service

Startup.cs: Markdig 라이브러리에서 구현된 AddMarkdown 메서드를 호출하여 Service에 등록한다.

public void ConfigureServices(IServiceCollection services)
{
    // 기존 Services
    ...

    // 신규 Service
    services.AddMarkdown(config =>
    {
        // Create custom MarkdigPipeline 
        // using MarkDig; for extension methods
        config.ConfigureMarkdigPipeline = builder =>
        {
            builder.UseEmphasisExtras(Markdig.Extensions.EmphasisExtras.EmphasisExtraOptions.Default)
                .UsePipeTables()
                .UseGridTables()
                .UseAutoIdentifiers(AutoIdentifierOptions.GitHub) // Headers get id="name" 
                .UseAutoLinks() // URLs are parsed into anchors
                .UseAbbreviations()
                .UseYamlFrontMatter()
                .UseEmojiAndSmiley(true)
                .UseListExtras()
                .UseFigures()
                .UseTaskLists()
                .UseCustomContainers()
                .UseMathematics()
                .UseGenericAttributes();
        };
    });
}

Modifying MDEditor razor cs file

MDEditor.razor.cs: MdEditor.razor는 위의 WebAssembly 내용과 동일하고 cs 파일만 하기와 같이 수정하면 된다. 정적 클래스로 만들어 둔 MarkdownParserFactory를 활용하는 것만 차이가 있다.

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Html;
using ArchivesdjWork2.Pages.Markdown;

namespace ArchivesdjWork2.Pages
{
    public partial class MDEditor : ComponentBase
    {
        public string Preview { get; set; }

        protected void OnTextChanged(ChangeEventArgs args)
        {
            Preview = Parse(args.Value.ToString());
            InvokeAsync(() => StateHasChanged());
        }

        /// <summary>
        /// Renders raw markdown from string to HTML
        /// </summary>
        /// <param name="markdown"></param>
        /// <param name="usePragmaLines"></param>
        /// <param name="forceReload"></param>
        /// <returns></returns>
        public static string Parse(string markdown, bool usePragmaLines = false, bool forceReload = false)
        {
           if (string.IsNullOrEmpty(markdown))
                return "";

            var parser = MarkdownParserFactory.GetParser(usePragmaLines, forceReload);
            return parser.Parse(markdown);
        }

        /// <summary>
        /// Renders raw Markdown from string to HTML.
        /// </summary>
        /// <param name="markdown"></param>
        /// <param name="usePragmaLines"></param>
        /// <param name="forceReload"></param>
        /// <returns></returns>
        public static HtmlString ParseHtmlString(string markdown, bool usePragmaLines = false, bool forceReload = false)
        {
            return new HtmlString(Parse(markdown, usePragmaLines, forceReload));
        }
    }
}

Summary

효율성을 위해 동적으로 Markdown 문서를 편집할 때는 두번째 방법을 사용하면 되지만, Blazor Server에서도 정적으로 md 문서를 열어서 보여 주기만 할때는 첫번째 방법이 이해하기도 쉽고 간단하다. Simple is the Best.


17 개의 글이 있습니다.

# 제목 날짜 조회수
01 CS 배우기 요약 2021/06/07 144
02 CS Statements 2021/06/07 127
03 퍼셉트론 2021/04/15 124
04 Blazor and Sqlite 2021/04/15 136
05 Blazor Layouts 2021/04/15 159
06 CS Language Reference 2021/06/07 125
07 VSCode and Markdown 2021/04/15 136
08 Blazor에서 이미지파일 다루기 2021/06/10 210
09 Blazor and Markdown 2021/04/15 144
10 종속성 주입 2021/06/07 151
11 Blazor에서 데이터 다루기 2021/06/07 137
12 Blazor Components 2021/04/15 146
13 CS Glossary 2021/06/07 125
14 Enum 타입 다루기 2021/12/14 134
15 생활코딩 CS01 2022/04/25 261
16 생활코딩 CS02 2022/04/30 165
17 생활코딩 CS03 2022/04/30 441

Most Popular #3

Recent #3

An error has occurred. This application may no longer respond until reloaded. Reload 🗙